Go 语言并发学习-sync 包-锁
Go 中的锁
虽然 Go 推荐使用 channel 进行线程之间的同步,但是有些场景还是使用锁比较方便。
Go 语言包中的 sync 包提供了两种锁类型:sync.Mutex
和 sync.RWMutex
,前者是互斥锁,后者是读写锁。
type Locker interface {
Lock()
Unlock()
}
互斥锁
标准库代码包 sync 中的 Mutex 结构体类型代表。只有两个公开方法:调用 Lock()
获得锁,调用 unlock()
释放锁。
使用 Lock()
加锁后,不能再继续对其加锁(同一个 goroutine 中,即:同步调用),否则会 panic。只有在 unlock()
之后才能再次 Lock()
。异步调用 Lock()
,是正当的锁竞争(即其它协程中调用),当然不会有 panic 了。适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁也叫做全局锁。
func (m *Mutex) Unlock()
用于解锁 m,如果在使用 Unlock()
前未加锁,就会引起一个运行错误。
使用例:
var lck sync.Mutex
func foo() {
lck.Lock()
defer lck.Unlock()
// ...
}
Mutex 也可以作为 struct 的一部分,这样这个 struct 就会防止被多线程更改数据。如下,编写一个原子计数器
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounter is safe to use concurrently.
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
c.v[key]++
c.mu.Unlock()
}
// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
defer c.mu.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
读写锁
读写锁和互斥锁不同之处在于,可以分别针对读操作和写操作进行分别锁定,这样对于性能有一定的提升。读写锁,对于多个写操作,以及写操作和读操作之前都是互斥的这一点基本等同于互斥锁。但是对于同时多个读操作之前却非互斥关系,这也是相读写锁性能高于互斥锁的主要原因。
基本遵循原则(其实就是读写锁的基本用途):
- 写锁定情况下,对读写锁进行读锁定或者写锁定,都将阻塞;而且读锁与写锁之间是互斥的;
- 读锁定情况下,对读写锁进行写锁定,将阻塞;加读锁时不会阻塞;
- 对未被写锁定的读写锁进行写解锁,会引发 Panic;
- 对未被读锁定的读写锁进行读解锁的时候也会引发 Panic;
- 写解锁在进行的同时会试图唤醒所有因进行读锁定而被阻塞的 goroutine;
- 读解锁在进行的时候则会试图唤醒一个因进行写锁定而被阻塞的 goroutine。
RWMutex 提供四个方法:
func (*RWMutex) Lock // 写锁定
func (*RWMutex) Unlock // 写解锁
func (*RWMutex) RLock // 读锁定
func (*RWMutex) RUnlock // 读解锁
使用示例:
package main
import (
"fmt"
)
var m *sync.RWMutex
func main() {
wg := sync.WaitGroup{}
wg.Add(20)
var rwMutex sync.RWMutex
Data := 0
for i := 0; i < 10; i++ {
go func() {
rwMutex.RLock()
defer rwMutex.RUnlock()
fmt.Printf("Read data: %v\n", Data)
wg.Done()
time.Sleep(2 * time.Second)
// 这句代码第一次运行后,读解锁。
// 循环到第二个时,读锁定后,这个goroutine就没有阻塞,同时读成功。
}()
go func(t int) {
rwMutex.Lock()
defer rwMutex.Unlock()
Data += t
fmt.Printf("Write Data: %v %d \n", Data, t)
wg.Done()
// 这句代码让写锁的效果显示出来,写锁定下是需要解锁后才能写的。
time.Sleep(2 * time.Second)
}(i)
}
time.Sleep(5 * time.Second)
wg.Wait()
}
WaitGroup 等待一组协程
官方文档对 WaitGroup 的描述是:一个 WaitGroup 对象可以等待一组协程结束。其实就是 Java 中的 CountDownLatch
- main 协程通过调用
wg.Add(delta int)
设置 worker 协程的个数,然后创建 worker 协程; - worker 协程执行结束以后,都要调用
wg.Done()
; - main 协程调用
wg.Wait()
且被 block,直到所有 worker 协程全部执行结束后返回。
使用例如下:
// src/cmd/compile/internal/ssa/gen/main.go
func main() {
// 省略部分代码 ...
var wg sync.WaitGroup
for _, task := range tasks {
task := task
wg.Add(1)
go func() {
task()
wg.Done()
}()
}
wg.Wait()
// 省略部分代码...
}
如何正确的使用读写锁
抛开业务来理解读写锁,它的本质是:
Lock()
时,会阻塞另一个协程Rlock()
和Lock()
Rlock()
时,不会阻塞另一个协程Rlock()
。但是会阻塞另一个协程的Lock()
如下使用,读的操作都加读锁,它可以阻塞写锁
package main
import (
"fmt"
"sync"
"time"
)
type SafeInteger struct {
m sync.RWMutex
data int
}
func (this *SafeInteger) readData() int {
this.m.RLock()
defer this.m.RUnlock()
time.Sleep(1 * time.Second)
return this.data
}
func (this *SafeInteger) writeData(val int) {
this.m.Lock()
defer this.m.Unlock()
this.data = val
time.Sleep(1 * time.Second)
fmt.Println("Write:", this.data)
}
func main() {
si := &SafeInteger{data: 10}
// 开 10 个协程用来读
for i := 0; i < 10; i++ {
go func() {
fmt.Println("Read:", si.readData())
}()
}
// 开 10 个协程用来读
for i := 0; i < 10; i++ {
go func(i int) {
si.writeData(i * 5)
}(i)
}
// 阻塞
select {}
}
如上代码,运行时可以发现读的操作不会被阻塞,写的操作才会被阻塞
锁的使用注意点
mutex 实例无需实例化,声明即可使用
func add(){
var mutex sync.Mutex
mutex.Lock()
defer mutex.Unlock()
fmt.Println("test lock")
}
mutex 在传递给外部使用的时候,需要传指针,不然传的是拷贝,会引起锁失败。并且指针的 mutex 是一定要实例化过的。
func add() *sync.Mutex {
var m = &sync.Mutex{}
return m
}
对同一个锁,进行多次锁,会死锁
func a(){
var mutex sync.Mutex
mutex.Lock()
mutex.Lock() // dead lock
}
对一个 RWLock 进行同时 Lock()
和 RLock()
会死锁.
func a(){
var mutex sync.RWMutex
mutex.RLock()
mutex.Lock() // dead lock
}
Go 中的重入锁 🚧
TODO: ...
如下代码函数 F()
和 G()
使用了相同的互斥锁,并且都在各自函数内部进行了加锁,这要使用就会出现死锁
func F() {
mu.Lock()
//... do some stuff ...
G()
//... do some more stuff ...
mu.Unlock()
}
func G() {
mu.Lock()
//... do some stuff ...
mu.Unlock()
}